/**
 @file: DomainDefinition.java
 
 PURPOSE:
 This class represents an instance of a DomainDefinition, either logical
 or physical.
 
 @author Ron Williams
 
 REVISION HISTORY ABSTRACT
 
 PROGRAMMER:  Ron Williams ORG: MSE
 
 Copyright 2011 Mission Solutions LLC. All rights reserved.
 
 END ABSTRACT
 */

package com.mse.utilities;

import java.lang.String;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.w3c.dom.*;                     // all XML DOM classes

import com.mse.utilities.Domain;
import com.mse.utilities.DomainDefLimits;
import com.mse.utilities.DomainHost;
import com.mse.utilities.DomainHostGroup;
import com.mse.utilities.DomainHostArchDef;
import com.mse.utilities.DomainException;
import com.mse.utilities.DomainId;
import com.mse.utilities.Log;
import com.mse.utilities.Utils;

/**
 DESCRIPTION:
 The DomainDefinition class contains the information as a result of 
 parsing a Domain Definition file. When successful, all domain information
 is held in a instance of "Domain".
 */
public class DomainDefinition implements org.xml.sax.ErrorHandler {
	
	/**
	 DESCRIPTION:
	 Create a DomainDefinition object based upon the Domain Definition
	 in the specified input file. It is assume the file path specified
	 is a valid and readible file.
	 
	 @param domDefPath - path to a domain definition file to parse and load
	 @return void
	 @throws DomainException
	 */
	public DomainDefinition(String domDefPath) throws DomainException {
		File ddFile = new File(domDefPath);
		
		if (ddFile.isFile() && ddFile.canRead()) {
			//theLog           = Log.getInstance();
			parse(ddFile);
			
			System.out.println("... Domain Definition file: " + domDefPath +
							   " successfully processed");
		} else {
			// bad file
			throw new DomainException("Domain File Invalid, does not exist/can not be read");
		}
	}
	
	public boolean getGoodFile(){
		return goodFile;
	}
	
	/**
	 DESCRIPTION:
	 get the Domain object resulting from parsing a DomainDefinition file.
	 
	 @return domain - the Domain object for the domain definition parsed
	 */
	public Domain getDomain() {
		return theDomain;
	}
	
	//---------------------------------------------------- ErrorHandler methods
	/**
	 DESCRIPTION:
	 XML parser invokes this method on recoverable errors.
	 
	 @param exception - the parse exception information
	 @return void
	 @throws SAXException
	 */
	public void error(SAXParseException exception) throws SAXException {
		throw new SAXException("Domain Definition problem: line: " +
							   exception.getLineNumber() + " Col: " +
							   exception.getColumnNumber() + "\n" +
							   exception.getMessage());
	}
	
	/**
	 DESCRIPTION:
	 XML parser invokes this method on fatal errors.
	 
	 @param exception - the parse exception information
	 @return void
	 @throws SAXException
	 */
	public void fatalError(SAXParseException exception) throws SAXException {
		throw new SAXException("Domain Definition Error: line: " +
							   exception.getLineNumber() + " Col: " +
							   exception.getColumnNumber() + "\n" +
							   exception.getMessage());
	}
	
	/**
	 DESCRIPTION:
	 XML parser invokes this method to report non-error information.
	 
	 @param exception - the parse exception information
	 @return void
	 @throws SAXException
	 */
	public void warning(SAXParseException exception) throws SAXException {
		throw new SAXException("Domain Definition warning: line: " +
							   exception.getLineNumber() + " Col: " +
							   exception.getColumnNumber() + "\n" +
							   exception.getMessage());
	}
	//------------------------------------------------ ErrorHandler methods END
	
	
	/**
	 DESCRIPTION:
	 Load a DomainDefinition into this object. If parsing is successful,
	 this object will hold a representation of the parsed domain entities
	 from the file, and the other methods are available to access them.
	 
	 @param domDef - domain definition file to parse and load
	 @return void
	 @throws DomainException
	 */
	private void parse(File domDef) throws DomainException {
		try {
			DocumentBuilderFactory DBF = DocumentBuilderFactory.newInstance();
			DBF.setXIncludeAware(true);
			DBF.setNamespaceAware(true);
			DBF.setIgnoringComments(true);
			
			DocumentBuilder docBld = DBF.newDocumentBuilder();
			docBld.setErrorHandler(this);    // this class handles parse errors!
			
			Document doc   = docBld.parse(domDef);
			Element domDefEl = doc.getDocumentElement();
			if (! tag_DomainDef.equals(domDefEl.getTagName()) ) {
				throw new DomainException("Error: Domain Definition does not " +
										  "seem to be a \"DomainDef\" document");
			}
			domDefEl.normalize();
			
			goodFile = parseDomainId(doc);
			goodFile = parseHostArchDefs(doc);
			goodFile = parseHostDefs(doc);
			goodFile = parseHostGroups(doc);
			
		} catch (ParserConfigurationException ex) {
			throw new DomainException("Failed to create parser", ex);
		} catch (IOException ex) {
			throw new DomainException("I/O error parsing file", ex);
		} catch (SAXException ex) {
			throw new DomainException("Failed to parse Domain Definition, " +
									  "reason:\n" + ex.getMessage());
		}
		
	}
	
	
	/**
	 DESCRIPTION:
	 parse a DomainID definition into this object. If parsing is successful,
	 the parsed DomainId entity is stored into the Domain representation.
	 
	 @param theDoc - DOM Document resulting from parse
	 @return void
	 @throws DomainException
	 */
	private boolean parseDomainId(Document theDoc) throws DomainException {
		
		NodeList domainIdElements = theDoc.getElementsByTagName(tag_DomainId);
		int      numElements      = domainIdElements.getLength();
		
		if (numElements < 1) {
			throw new DomainException("Error: Invalid Domain Definition, one " +
									  "\"DomainId\" element definition must be specified");
		} else if (numElements > 1) {
			throw new DomainException("Error: Invalid Domain Definition, only " +
									  "ONE \"DomainId\" element definition must be specified");
		}
		
		// only 1 exists (it must be a DomainID element)
		NodeList fields = domainIdElements.item(0).getChildNodes();
		
		// hold the element fields, which we create the DomainID object with
		String field_DID         = "";
		String field_DomainType  = "";
		String field_DisplayName = "";
		String field_Version     = "";
		String field_major       = "";
		String field_minor       = "";
		
		Node    aNode;
		Node    textNode;
		Text    text;
		Element el;
		String  textData;
		
		// process all child element tags (fields of the DomainId)
		for (int i=0; i < fields.getLength(); i++) {
			aNode = fields.item(i);
			// process all element tags only
			if (Node.ELEMENT_NODE == aNode.getNodeType()) {
				// only expect one item to follow each element, the "text" node
				// containing the value for the element
				textNode = aNode.getFirstChild();
				el       = (Element)aNode;
				text     = null;
				textData = "";
				
				if ((textNode != null) &&
					( textNode.getNodeType() == Node.TEXT_NODE)) {
					// it is a text node, so get the text value
					text     = (Text)textNode;
					textData = text.getData().trim();
				}
				
				if ("DID".equals(el.getTagName()) ) {
					field_DID = textData;
				} else if ("DomainType".equals(el.getTagName()) ) {
					field_DomainType = textData;
				} else if ("DisplayName".equals(el.getTagName()) ) {
					field_DisplayName = textData;
				} else if ("version".equals(el.getTagName()) ) {
					field_Version = textData;
				} else if ("major".equals(el.getTagName()) ) {
					field_major = textData;
				} else if ("minor".equals(el.getTagName()) ) {
					field_minor = textData;
				} else {
					throw new DomainException("Error: Invalid Domain Definition, " +
											  "DomainId has invalid element: " +
											  el.getTagName());
				}
			}
		}
		
		// verify all DomainId fields were specified (they all must not be null)
		if (field_DID.isEmpty()         ||
			field_DomainType.isEmpty()  ||
			field_DisplayName.isEmpty() ||
			field_Version.isEmpty()     ||
			field_major.isEmpty()       ||
			field_minor.isEmpty()       ) {
			throw new DomainException("Error: Invalid Domain Definition, " +
									  "DomainID has missing or elements with null values");
		} 
		
		DomainId ourDID = new DomainId(field_DID,         field_DomainType,
									   field_DisplayName, field_Version,
									   field_major,       field_minor);
		theDomain = new Domain(ourDID);
		
		// valid DomainID definition
		
		System.out.println("... DomainID is valid:\n" + ourDID.toString());
		return true;
	}
	
	/**
	 DESCRIPTION:
	 parse all HostArchDef elements from the DomainDefinition. If parsing
	 is successful, all HostArchDef objects are stored into the Domain.
	 
	 @param theDoc - DOM Document resulting from parse
	 @return void
	 @throws DomainException
	 */
	private boolean parseHostArchDefs(Document theDoc) throws DomainException {
		boolean result = false;
		NodeList hostArchElements = theDoc.getElementsByTagName(tag_HostArchDef);
		
		// hold the element fields, which we create DomainHostArchDef objects with
		String field_ArchId = "";
		String field_OSType = "";
		String field_OSVer  = "";
		String field_Arch   = "";
		
		Node     aNode;
		Node     textNode;
		NodeList fields;
		Text     text;
		Element  el;
		String   textData;
		DomainHostArchDef archDef;
		
		// process all HostArchDef element tags
		for (int i=0; i < hostArchElements.getLength(); i++) {
			aNode = hostArchElements.item(i);
			if (Node.ELEMENT_NODE == aNode.getNodeType()) {
				fields = aNode.getChildNodes();
				field_ArchId = "";
				field_OSType = "";
				field_OSVer  = "";
				field_Arch   = "";
				// process all child element tags for this HostArchDef element
				for (int j=0; j < fields.getLength(); j++) {
					aNode = fields.item(j);
					// process all element tags only
					if (Node.ELEMENT_NODE == aNode.getNodeType()) {
						// only expect one item to follow each element, a "text" node
						// containing the value for the element
						textNode = aNode.getFirstChild();
						el       = (Element)aNode;
						text     = null;
						textData = "";
						
						if ((textNode != null) &&
							(textNode.getNodeType() == Node.TEXT_NODE)) {
							// it is a text node, so get the text value
							text     = (Text)textNode;
							textData = text.getData().trim();
						}
						
						if ("ArchID".equals(el.getTagName()) ) {
							field_ArchId = textData;
						} else if ("osType".equals(el.getTagName()) ) {
							field_OSType = textData;
						} else if ("osVer".equals(el.getTagName()) ) {
							field_OSVer = textData;
						} else if ("arch".equals(el.getTagName()) ) {
							field_Arch = textData;
						} else {
							throw new DomainException("Error: HostArchDef " +
													  "definition has invalid element: " + el.getTagName());
						}
					}
				} // end-for each HostArchDef child element tag
				
				// verify all fields were specified (they all must not be null)
				if (field_ArchId.isEmpty() ||
					field_OSType.isEmpty() ||
					field_OSVer.isEmpty()  ||
					field_Arch.isEmpty()  ) {
					throw new DomainException("Error: Invalid Domain Definition, " +
											  "a HostArchDef has missing or elements with null values");
				} 
				
				archDef = new DomainHostArchDef(field_ArchId, field_OSType,
												field_OSVer,  field_Arch);
				theDomain.addHostArchDef(archDef);
				
				// valid host architecture definition
				result = true;
				System.out.println("... DomainHostArchDef is valid:\n" + 
								   archDef.toString());
			}
		} // end-for each HostArchDef element tag
		return result;
	}
	
	
	/**
	 DESCRIPTION:
	 parse all HostDef elements from the DomainDefinition. If parsing
	 is successful, all HostDef objects are stored into the Domain.
	 
	 @param theDoc - DOM Document resulting from parse
	 @return void
	 @throws DomainException
	 */
	private boolean parseHostDefs(Document theDoc) throws DomainException {
		boolean result = false;
		NodeList hostElements = theDoc.getElementsByTagName(tag_HostDef);
		
		// hold the element fields, which we create DomainHostDef objects with
		String field_HID      = "";
		String field_HostName = "";
		String field_HostArch = "";
		
		Node     aNode;
		Node     textNode;
		NodeList fields;
		Text     text;
		Element  el;
		String   textData;
		DomainHost hostDef;
		
		// process all HostDef element tags
		for (int i=0; i < hostElements.getLength(); i++) {
			aNode = hostElements.item(i);
			if (Node.ELEMENT_NODE == aNode.getNodeType()) {
				fields = aNode.getChildNodes();
				field_HID      = "";
				field_HostName = "";
				field_HostArch = "";
				
				// process all child element tags for this HostDef element
				for (int j=0; j < fields.getLength(); j++) {
					aNode = fields.item(j);
					// process all element tags only
					if (Node.ELEMENT_NODE == aNode.getNodeType()) {
						// only expect one item to follow each element, a "text" node
						// containing the value for the element
						textNode = aNode.getFirstChild();
						el       = (Element)aNode;
						text     = null;
						textData = "";
						
						if ((textNode != null) &&
							(textNode.getNodeType() == Node.TEXT_NODE)) {
							// it is a text node, so get the text value
							text     = (Text)textNode;
							textData = text.getData().trim();
						}
						
						if ("HID".equals(el.getTagName()) ) {
							field_HID = textData;
						} else if ("hostName".equals(el.getTagName()) ) {
							field_HostName = textData;
						} else if ("HostArch".equals(el.getTagName()) ) {
							field_HostArch = textData;
						} else {
							throw new DomainException("Error: HostDef definition " +
													  "has invalid element: " + el.getTagName());
						}
					}
				} // end-for each HostDef child element tag
				
				// verify all fields were specified (hostName is optional for a
				// logical domain)
				if (field_HID.isEmpty() || field_HostArch.isEmpty() ) {
					throw new DomainException("Error: Invalid Domain Definition, " +
											  "a HostDef has missing or elements with null values");
				} else if ((! theDomain.isDomainLogical()) &&
						   field_HostName.isEmpty() ) {
					throw new DomainException("Error: Invalid Physical Domain " +
											  "Definition, HostDef: " + field_HID + 
											  " is missing the hostName field");
				}
				
				if (theDomain.isDomainLogical() ) { 
					field_HostName = "";       // forcing blank in logical domains
				} else if (! Utils.isFileSystemName(field_HostName)) {
					throw new DomainException("Error: Invalid Domain Definition, " +
											  "HostDef: " + field_HID +
											  ", hostName field value has unsupported characters");
				}
				
				DomainHostArchDef archDef = theDomain.
				getHostArchDef(field_HostArch);
				if (null == archDef) {
					throw new DomainException("Error: Invalid Domain Definition, " +
											  "HostDef: " + field_HID + 
											  " has invalid HostArch value.\n" +
											  "      Value must be defined in a HostArchDef.");
				}
				
				hostDef = new DomainHost(field_HID, field_HostName, archDef);
				
				theDomain.addHostDef(hostDef);
				
				// valid host definition
				result = true;
				System.out.println("... HostDef is valid:\n" + hostDef.toString());
			}
		} // end-for each HostDef element tag
		return result;
	}
	
	
	/**
	 DESCRIPTION:
	 parse all HostGroup elements from the DomainDefinition. If parsing
	 is successful, all HostGroup objects are stored into the Domain.
	 
	 @param theDoc - DOM Document resulting from parse
	 @return void
	 @throws DomainException
	 */
	private boolean parseHostGroups(Document theDoc) throws DomainException {
		boolean result = false;
		NodeList groupElements = theDoc.getElementsByTagName(tag_HostGroup);
		
		// hold the element fields, which we create DomainHostGroup objects with
		String field_HGID      = "";
		String field_GroupName = "";
		String field_Host      = "";
		
		Node     aNode;
		Node     textNode;
		NodeList fields;
		Text     text;
		Element  el;
		String   textData;
		DomainHostGroup       aGroup;
		ArrayList<DomainHost> hostList = new ArrayList<DomainHost>();
		DomainHost aHost;
		
		// process all HostGroup element tags
		for (int i=0; i < groupElements.getLength(); i++) {
			aNode = groupElements.item(i);
			if (Node.ELEMENT_NODE == aNode.getNodeType()) {
				fields = aNode.getChildNodes();
				field_HGID      = "";
				field_GroupName = "";
				field_Host      = "";
				
				// process all child element tags for this HostGroup element
				for (int j=0; j < fields.getLength(); j++) {
					aNode = fields.item(j);
					// process all element tags only
					if (Node.ELEMENT_NODE == aNode.getNodeType()) {
						// only expect one item to follow each element, a "text" node
						// containing the value for the element
						textNode = aNode.getFirstChild();
						el       = (Element)aNode;
						text     = null;
						textData = "";
						
						if ((textNode != null) &&
							(textNode.getNodeType() == Node.TEXT_NODE)) {
							// it is a text node, so get the text value
							text     = (Text)textNode;
							textData = text.getData().trim();
						}
						
						if ("HGID".equals(el.getTagName()) ) {
							field_HGID = textData;
						} else if ("groupName".equals(el.getTagName()) ) {
							field_GroupName = textData;
						} else if ("host".equals(el.getTagName()) ) {
							field_Host = textData;
							aHost = theDomain.getHostDef(field_Host);
							if (null == aHost) {
								throw new DomainException("Error: Invalid Domain Definition, " +
														  
														  "HostGroup definition has invalid host: " + 
														  field_Host);
							}
							hostList.add(aHost);
						} else {
							throw new DomainException("Error: HostGroup definition " +
													  "has invalid element: " + el.getTagName());
						}
					}
				} // end-for each HostGroup child element tag
				
				// verify all fields were specified 
				if (field_HGID.isEmpty()      ||
					field_GroupName.isEmpty() ||
					(hostList.size() < 1) ) {
					throw new DomainException("Error: Invalid Domain Definition, " +
											  "a HostGroup has missing or elements with null values");
				}
				
				aGroup = new DomainHostGroup(field_HGID, field_GroupName);
				for (DomainHost theHost : hostList) {
					aGroup.addHost(theHost);
				}
				
				theDomain.addHostGroup(aGroup);
				
				hostList.clear();
				
				// valid host definition
				result = true;
				System.out.println("... HostGroup is valid:\n" + aGroup.toString());
			}
		} // end-for each HostGroup element tag
		
		// verify that every host has been placed into one group
		return (result && validateHostsInGroup());
	}
	
	
	/**
	 DESCRIPTION:
	 verify that every host has been placed into one group.
	 
	 @return void
	 @throws DomainException if there is a problem
	 */
	private boolean validateHostsInGroup() throws DomainException {
		boolean result = false;
		List<DomainHost> hosts  = theDomain.getDomainHosts();
		DomainHostGroup  aGroup = null;
		
		for (DomainHost aHost : hosts) {
			if ((aGroup = theDomain.getGroupForHost(aHost)) == null) {
				throw new DomainException("Error: HostDef with HID: " +
										  aHost.getHID() + " has NOT been assigned to a HostGroup");
			}
			result = true;
		}
		return result;
	}
	
	
	private static final String tag_DomainDef   = "DomainDef";
	private static final String tag_DomainId    = "DomainID";
	private static final String tag_HostArchDef = "HostArchDef";
	private static final String tag_HostDef     = "HostDef";
	private static final String tag_HostGroup   = "HostGroup";
	
	
	private Domain theDomain;  // the Domain object resulting from all parsing
	private boolean goodFile = false;
}

